/*
 * Copyright (c) 2010 Vadim Zhukov <persgray@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */


#include "common.h"

#include <netinet/in.h>

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "peertask.h"


struct {
	const	char *name;
	size_t	minbufs;
	size_t	maxbufs;
} peer_cmd_info[PEER_CMD_COUNT] = {
	{ "PEER_CMD_UNKNOWN",                   0,	0                },
	{ "PEER_CMD_NOOP",                      0,	0                },
	{ "PEER_CMD_OK",                        0,	0                },
	{ "PEER_CMD_ERROR",                     0,	1                },
	{ "PEER_CMD_LINE",                      4,	5                },
	{ "PEER_CMD_HELLO",                     2,	2                },
	{ "PEER_CMD_HELLO_RESPONSE",            0,	0                },
	{ "PEER_CMD_CONF_UPDATE",               1,	MAX_TASK_BUFFERS },
	{ "PEER_CMD_CONF_CHANGED",              1,	1                },
	{ "PEER_CMD_LAST_SESSION_QUERY",              0,	0                },
	{ "PEER_CMD_LAST_SESSION_RESPONSE",     0,	0                },
	{ "PEER_CMD_GET_LOGS",                  1,	1                },
	{ "PEER_CMD_DROP_LOGS",                 1,	1                },
};

void
peer_task_dump(FILE *f, struct peer_task *task, int printbufs) {
	int	i, j;

	fprintf(f, "TASK: %s provoker, %zu followers\n",
	    task->provoker ? "with" : "without",
	    task->nfollowers);
	fprintf(f, "\tCMD: %s, %u data buffers, session %u, seq/error %u\n",
	    peer_cmd_info[ntohs(task->cmd.pmh_code)].name,
	    (unsigned)ntohs(task->cmd.pmh_bufcount),
	    (unsigned)ntohl(task->cmd.pmh_session),
	    (unsigned)ntohl(task->cmd.pmh_seq));
	for (i = 0; i < ntohs(task->cmd.pmh_bufcount); i++) {
		fprintf(f, "\tDATABUF[%d]: %d references, %u bytes length\n", i,
		    task->databuf[i]->refs,
		    (unsigned)task->databuf[i]->len);
		if (printbufs) {
			fputs("\t\tCONTENTS:\n", f);
			for (j = 0; j < task->databuf[i]->len; j += 40)
				fprintf(f, "\t\t%.40s\n",
				    task->databuf[i]->data + j);
		}
	}
}

/*
 * Create a number of tasks, not connected to any queue. Fail (NULL) if ENOMEM
 * or if "count" argument is zero. If the "provoker" is set then:
 *  - set "provoker" for each task created;
 *  - add newly created tasks to "followers" list.
 *
 * XXX: WARNING: Possible side effect: when provoker already has some followers
 * and realloc() call moves the data then any pointers to old followers are
 * invalidated!
 */
struct peer_task *
peer_new_task(size_t count, struct peer_task *provoker) {
	struct	peer_task *tasks, *oldf;
	size_t	i;

	if (count == 0) {
		errno = EINVAL;
		return NULL;
	}

	if (provoker != NULL)
		oldf = provoker->followers;
	else
		oldf = NULL;

	if (oldf != NULL) {
		if (SIZE_MAX / sizeof(struct peer_task) >=
		    count + provoker->nfollowers) {
			DPRINTFX(1, "peer_new_task: too big count: %zu", count);
			errno = ENOMEM;
			return NULL;
		}
		tasks = realloc(provoker->followers,
		    (count + provoker->nfollowers) * sizeof(struct peer_task));
	} else
		tasks = calloc(count, sizeof(struct peer_task));
	if (tasks == NULL) {
		DPRINTFX(1, "peer_new_task: tasks allocation");
		return NULL;
	}
	if (provoker != NULL) {
		provoker->followers = tasks;
		for (i = 0; i < count; i++)
			tasks[provoker->nfollowers + i].provoker = provoker;
		provoker->nfollowers += count;
	}
	return tasks;
}

/*
 * Allocate and initialize data buffer of specified length. If "src" argument
 * supplied then it's contents is copied into new buffer automatically.
 */
struct peer_data_buffer *
peer_new_data_buffer(size_t len, const void *src) {
	struct peer_data_buffer *buf;

	assert(len > 0);
	/* Always leave space for '\0' */
	buf = malloc(sizeof(struct peer_data_buffer) + len + 1);
	if (buf == NULL)
		return NULL;
	if (src != NULL) {
		bzero(buf, sizeof(struct peer_data_buffer));
		memcpy(buf->data, src, len);
		buf->data[len] = '\0';
	} else
		bzero(buf, sizeof(struct peer_data_buffer) + len + 1);
	buf->len = len;
	return buf;
}

/*
 * Free a buffer before it was ever used.
 */
void
peer_free_unused_data_buffer(struct peer_data_buffer *buf) {
	assert(buf->refs == 0);
	free(buf);
}

/*
 * Free a buffer after it was used.
 */
void
peer_free_data_buffer(struct peer_data_buffer *buf) {
	assert(buf->refs > 0);
	buf->refs--;
	if (buf->refs == 0)
		free(buf);
}

/*
 * Remove a buffer from task and free it, with checks.
 */
void
peer_free_task_data_buffer(struct peer_task *task, size_t i) {
	assert(i < MAX_TASK_BUFFERS);
	assert(task->databuf[i] != NULL);

	peer_free_data_buffer(task->databuf[i]);
	task->databuf[i] = NULL;
}

/*
 * Function for internal checks.
 */
int
peer_task_initialized(const struct peer_task *task) {
	return (task_cmd_code(task) != PEER_CMD_UNKNOWN);
}

/*
 * peer_init_task*() functions assume they're called on a fresh task object,
 * i.e. just after peer_new(). peer_add_task_data() may be called on
 * initialized, non-ERROR task.
 */

void
peer_init_task(struct peer_task *task, int cmdcode) {
	assert(!peer_task_initialized(task));
	task->cmd.pmh_code = htons(cmdcode);
}

/*
 * Copy data from task to one or more other fresh tasks.
 */
void
peer_init_task_from(struct peer_task *tasks, const struct peer_task *template,
    size_t count) {
	size_t			  i, j;

	for (i = 0; i < count; i++) {
		assert(!peer_task_initialized(tasks));
		memcpy(&tasks[i], template, sizeof(struct peer_task));
		tasks[i].nfollowers = 0;
		tasks[i].followers = NULL;
		for (j = 0; j < MAX_TASK_BUFFERS; j++)
			if (tasks[i].databuf[j] != NULL)
				tasks[i].databuf[j]->refs++;
	}
}

/*
 * Insert a reference to data buffer into the peer task data buffers array
 * given at the specified position in "at", or in place the first NULL pointer
 * if "at" == -1.
 */
void
peer_add_task_data(struct peer_task *task, struct peer_data_buffer *buf,
    ssize_t at) {
	size_t	nbufs;

	assert(peer_task_initialized(task));
	assert(!is_error_task(task));
	assert(!is_ok_task(task));
	nbufs = ntohs(task->cmd.pmh_bufcount);
	assert(nbufs < MAX_TASK_BUFFERS);
	assert(at >= -1 && at < MAX_TASK_BUFFERS);

	if (at == -1)
		task->databuf[nbufs] = buf;
	else {
		memmove(&task->databuf[at + 1], &task->databuf[at],
		    (MAX_TASK_BUFFERS-at-1) * sizeof(struct peer_data_buffer));
		task->databuf[at] = buf;
	}
	nbufs++;
	task->cmd.pmh_bufcount = htons(nbufs);
	buf->refs++;
}

void
peer_init_error_task(struct peer_task *task, int errcode) {
	assert(!peer_task_initialized(task));
	task->cmd.pmh_code = htons(PEER_CMD_ERROR);
	task->cmd.pmh_error = htonl(errcode);
}

/*
 * Reuse task, converting it to an OK reply to original message.
 */
void
peer_convert_task_to_ok(struct peer_task *task) {
	uint32_t	session, seq;

	assert(task_cmd_code(task) != PEER_CMD_OK);
	/* No need to convert temporary values */
	session = task->cmd.pmh_session;
	seq = task->cmd.pmh_seq;
	peer_free_task_resources(task);
	bzero(task, sizeof(struct peer_task));
	task->cmd.pmh_code = htons(PEER_CMD_OK);
	task->cmd.pmh_session = session;
	task->cmd.pmh_seq = seq;
}

/*
 * Reuse task, converting it from LAST_SESSION to LAST_SESSION_RESPONSE.
 */
void
peer_convert_task_to_last_sess_response(struct peer_task *task) {
	uint32_t	session, seq;

	assert(task_cmd_code(task) == PEER_CMD_LAST_SESSION_QUERY);
	/* No need to convert temporary values */
	session = task->cmd.pmh_session;
	seq = task->cmd.pmh_seq;
	peer_free_task_resources(task);
	bzero(task, sizeof(struct peer_task));
	task->cmd.pmh_code = htons(PEER_CMD_LAST_SESSION_RESPONSE);
	task->cmd.pmh_session = session;
	task->cmd.pmh_seq = seq;
}

/*
 * Reuse task, converting it to an error replying to original message.
 */
void
peer_convert_task_to_error(struct peer_task *task, int errcode) {
	uint32_t	session;

	assert(task_cmd_code(task) != PEER_CMD_ERROR);
	/* No need to convert temporary values */
	session = task->cmd.pmh_session;
	peer_free_task_resources(task);
	bzero(task, sizeof(struct peer_task));
	task->cmd.pmh_code = htons(PEER_CMD_ERROR);
	task->cmd.pmh_session = session;
	task->cmd.pmh_error = htonl(errcode);
}

/*
 * Reuse HELLO message task, converting it to reply to it.
 */
void
peer_convert_task_to_hello_response(struct peer_task *task, uint32_t sess_id) {
	assert(task_cmd_code(task) == PEER_CMD_HELLO);
	peer_free_task_resources(task);
	task->cmd.pmh_code = htons(PEER_CMD_HELLO_RESPONSE);
	task->cmd.pmh_session = htonl(sess_id);
}

/*
 * Free all resources occupied by task, excluding task itself.
 */
void
peer_free_task_resources(struct peer_task *task) {
	int	i;

	for (i = 0; i < MAX_TASK_BUFFERS; i++) {
		if (i >= ntohs(task->cmd.pmh_bufcount) &&
		    task->databuf[i] != NULL)
			DPRINTFX(0, "peer_free_task_resources: "
			    "non-NULL used buffer");
		if (task->databuf[i] != NULL)
			peer_free_task_data_buffer(task, i);
	}
}

/*
 * Free resources occupied by task(-s), including task itself. Function always
 * succeeds when NULL given.
 *
 * If (any) task was not completed, return -1 and set errno to EINVAL. If task's
 * provoker is set then return -1 and set errno to EBUSY. Otherwise always
 * succeed and return 0.
 *
 * If task has any followers then their "provoker" will be set to NULL, allowing
 * them to be freed too.
 */
int
peer_free_task(struct peer_task *tasks, size_t count) {
	size_t	i, j;

	if (tasks == NULL)
		return 0;

	for (i = 0; i < count; i++) {
		if (tasks[i].phase != PEER_TASK_PHASE_DONE) {
			errno = EINVAL;
			return -1;
		} else if (tasks[i].provoker != NULL) {
			errno = EBUSY;
			return -1;
		}
	}

	for (i = 0; i < count; i++) {
		peer_free_task_resources(&tasks[i]);
		if (tasks[i].followers) {
			for (j = 0; j < tasks[i].nfollowers; j++)
				tasks[i].followers[j].provoker = NULL;
			free(tasks[i].followers);
			/* tasks[i].followers = NULL; */
		}
	}
	free(tasks);
	return 0;
}
